#!/usr/bin/env python
#
# Copyright 2007 Dundee Media & Technology, Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
"""functions to help with converting from UTC to the clients local timezone
when working on App Engine projects"""
__revision__ = "$Rev$"

import datetime

# -- dictionary of timezone information for use with Aem_tzinfo Class
# source data: http://home.tiscali.nl/~t876506/TZworld.html
AEM_TZ_DATA = {
    'UTC':{'desc':"Implementation of the UTC timezone.",
               'utc_offset':0,          # +/- offset from UTC
               'tzname':'UTC',          # Primary TZ name
               'dst_tzname':'',         # daylight savings variant TZ Name,
                                        #   blank if area doesn't support DST
               'dst_start_month':0,     # Month # that DST period begins, '' if
                                        #   area doesn't use DST
               'dst_start_sunday':0,    # Which Sunday, 1-4 or 5=last sunday of
                                        #   month, 0 if area doesn't use DST
               'dst_start_hour':0,      # 1-24 dst starting hour in 24hour
                                        #   format, 0 if area doesn't use DST
               'dst_end_month':0,       # Month # that DST period ends, '' if
                                        #   area doesn't use DST
               'dst_end_sunday':0,      # Which Sunday, 1-4 or 5=last sunday of
                                        #   month, 0 if area doesn't use DST
               'dst_end_hour':0},       # 1-24 dst ending hour in 24hour format,
                                        #   0 if area doesn't use DST
    'US/Pacific':{'desc':"Implementation of the Pacific timezone.",
               'utc_offset':-8,
               'tzname':'PST',
               'dst_tzname':'PDT',
               'dst_start_month':3,
               'dst_start_sunday':2,
               'dst_start_hour':2,
               'dst_end_month':11,
               'dst_end_sunday':1,
               'dst_end_hour':1},
    'US/Mountain':{'desc':"Implementation of the Mountain timezone.",
               'utc_offset':-7,
               'tzname':'MST',
               'dst_tzname':'MDT',
               'dst_start_month':3,
               'dst_start_sunday':2,
               'dst_start_hour':2,
               'dst_end_month':11,
               'dst_end_sunday':1,
               'dst_end_hour':1},
    'US/Central':{'desc':"Implementation of the Central timezone.",
               'utc_offset':-6,
               'tzname':'CST',
               'dst_tzname':'CDT',
               'dst_start_month':3,
               'dst_start_sunday':2,
               'dst_start_hour':2,
               'dst_end_month':11,
               'dst_end_sunday':1,
               'dst_end_hour':1},
    'US/Eastern':{'desc':"Implementation of the Eastern timezone.",
               'utc_offset':-5,
               'tzname':'EST',
               'dst_tzname':'EDT',
               'dst_start_month':3,
               'dst_start_sunday':2,
               'dst_start_hour':2,
               'dst_end_month':11,
               'dst_end_sunday':1,
               'dst_end_hour':1},
    'America/Phoenix':{'desc':"Implementation of Arizona, most of timezone.",
               'utc_offset':-7,
               'tzname':'MST',
               'dst_tzname':'',
               'dst_start_month':0,
               'dst_start_sunday':0,
               'dst_start_hour':0,
               'dst_end_month':0,
               'dst_end_sunday':0,
               'dst_end_hour':0},
    'America/Indianapolis':{'desc':"Implementation of Indiana timezone.",
               'utc_offset':-5,
               'tzname':'EST',
               'dst_tzname':'',
               'dst_start_month':0,
               'dst_start_sunday':0,
               'dst_start_hour':0,
               'dst_end_month':0,
               'dst_end_sunday':0,
               'dst_end_hour':0},
    'Europe/Paris':{'desc':"Implementation of Europe/Paris timezone.",
               'utc_offset':1,
               'tzname':'CET',
               'dst_tzname':'CEST',
               'dst_start_month':3,
               'dst_start_sunday':5,
               'dst_start_hour':1,
               'dst_end_month':10,
               'dst_end_sunday':5,
               'dst_end_hour':1},
    'Europe/Berlin':{'desc':"Implementation of Europe/Berlin timezone.",
               'utc_offset':1,
               'tzname':'CET',
               'dst_tzname':'CEST',
               'dst_start_month':3,
               'dst_start_sunday':5,
               'dst_start_hour':1,
               'dst_end_month':10,
               'dst_end_sunday':5,
               'dst_end_hour':1},
    'Europe/Athens':{'desc':"Implementation of Europe/Athens timezone.",
               'utc_offset':2,
               'tzname':'EET',
               'dst_tzname':'EEST',
               'dst_start_month':3,
               'dst_start_sunday':5,
               'dst_start_hour':1,
               'dst_end_month':10,
               'dst_end_sunday':5,
               'dst_end_hour':1},
    'Asia/Calcutta':{'desc':"Implementation of Asia/Calcutta (India) timezone.",
               'utc_offset':5.5,
               'tzname':'IST',
               'dst_tzname':'',
               'dst_start_month':0,
               'dst_start_sunday':0,
               'dst_start_hour':0,
               'dst_end_month':0,
               'dst_end_sunday':0,
               'dst_end_hour':0},
    'Asia/Hong_Kong':{'desc':"Implementation of Asia/Hong_Kong timezone.",
               'utc_offset':8,
               'tzname':'HKT',
               'dst_tzname':'',
               'dst_start_month':0,
               'dst_start_sunday':0,
               'dst_start_hour':0,
               'dst_end_month':0,
               'dst_end_sunday':0,
               'dst_end_hour':0},
    'Asia/Tokyo':{'desc':"Implementation of Asia/Tokyo timezone.",
               'utc_offset':9,
               'tzname':'JST',
               'dst_tzname':'',
               'dst_start_month':0,
               'dst_start_sunday':0,
               'dst_start_hour':0,
               'dst_end_month':0,
               'dst_end_sunday':0,
               'dst_end_hour':0},
    'Australia/Darwin':{'desc':"Implementation of Australia/Darwin timezone.",
               'utc_offset':9.5,
               'tzname':'CST',
               'dst_tzname':'',
               'dst_start_month':0,
               'dst_start_sunday':0,
               'dst_start_hour':0,
               'dst_end_month':0,
               'dst_end_sunday':0,
               'dst_end_hour':0},
    'Australia/Perth':{'desc':"Implementation of Australia/Perth timezone.",
               'utc_offset':8,
               'tzname':'WST',
               'dst_tzname':'',
               'dst_start_month':10,
               'dst_start_sunday':5,
               'dst_start_hour':2,
               'dst_end_month':3,
               'dst_end_sunday':5,
               'dst_end_hour':2},
    'Australia/Eucla':{'desc':"Implementation of Australia/Eucla timezone.",
               'utc_offset':8.75,
               'tzname':'CWST',
               'dst_tzname':'',
               'dst_start_month':10,
               'dst_start_sunday':5,
               'dst_start_hour':2,
               'dst_end_month':3,
               'dst_end_sunday':5,
               'dst_end_hour':2},
    'Pacific/Fiji':{'desc':"Implementation of Pacific/Fiji timezone.",
               'utc_offset':12,
               'tzname':'FJT',
               'dst_tzname':'',
               'dst_start_month':0,
               'dst_start_sunday':0,
               'dst_start_hour':0,
               'dst_end_month':0,
               'dst_end_sunday':0,
               'dst_end_hour':0},
}

# NOTE: Aem_tzinfo is should not be pickled per
# http://python.org/doc/lib/datetime-tzinfo.html because the __init__ should
# not have parameters and the Aem_tzinfo does have a parameter, the AEM_TZ_DATA
# element to use.  However, since this module was written for use with Google's
# App Engine, this should not present a problem since GAE stores and returns
# UTC date/times only.  When building a GAE app, you should also store all date/
# time information as UTC too.  Aem_tzinfo is for presentation only.

class Aem_tzinfo(datetime.tzinfo):      # pylint: disable-msg=C0103,R0902
    """aemechanic timezone implementation."""
    def __init__(self, tz_datum):       # pylint: disable-msg=W0231
        '''init internal vectors with aemechanic_tz_data element specified by
        user'''
        #default to US/Central time
        self.utc_offset = tz_datum.get('utc_offset', -6) 
        self.tznom = tz_datum.get('tzname', 'CST')
        self.dst_start_month = tz_datum.get('dst_start_month', 3)
        if tz_datum.get('dst_start_sunday', 2) == 5:
            day_of_month = 25   #last sunday of month is on or after the 25th
        else:
            day_of_month = ((tz_datum.get('dst_start_sunday', 2) - 1) * 7) + 1
        self.dst_start_sunday = day_of_month
        self.dst_start_hour = tz_datum.get('dst_start_hour', 2)
        self.dst_end_month = tz_datum.get('dst_end_month', 11)
        if tz_datum.get('dst_end_sunday', 2) == 5:
            day_of_month = 25   #last sunday of month is on or after the 25th
        else:
            day_of_month = ((tz_datum.get('dst_end_sunday', 2) - 1) * 7) + 1
        self.dst_end_sunday = day_of_month
        self.dst_end_hour = tz_datum.get('dst_end_hour', 1)
        self.dst_tznom = tz_datum.get('dst_tzname', 'CDT')
        
    def utcoffset(self, dtz):
        '''calculate the difference plus any necessary modification due to
        daylight savings and return'''
        return datetime.timedelta(hours=self.utc_offset) + self.dst(dtz)
    
    def _FirstSunday(self, dtz):        # pylint: disable-msg=C0103,R0201
        """First Sunday on or after dt."""
        return dtz + datetime.timedelta(days=(6-dtz.weekday()))
    
    def dst(self, dtz):
        '''determine if "now" is in a a daylight savings period and if so,
        modify the returned date.  If the timezone does not implement daylight
        savings then return time without modification'''
        #short circuit if time zone does not implements daylight savings
        if self.dst_start_month == 0:
            return datetime.timedelta(hours=0)
        
        # 2 am on the second Sunday in March
        dst_start = self._FirstSunday(datetime.datetime(
                                                        dtz.year,
                                                        self.dst_start_month,
                                                        self.dst_start_sunday,
                                                        self.dst_start_hour))
        # 1 am on the first Sunday in November
        dst_end = self._FirstSunday(datetime.datetime(
                                                        dtz.year,
                                                        self.dst_end_month,
                                                        self.dst_end_sunday,
                                                        self.dst_end_hour))
    
        if dst_start <= dtz.replace(tzinfo=None) < dst_end:
            return datetime.timedelta(hours=1)
        else:
            return datetime.timedelta(hours=0)
    
    def tzname(self, dtz):
        '''return the proper tzname based on difference between dst and now'''
        if self.dst(dtz) == datetime.timedelta(hours=0):
            return self.tznom
        else:
            return self.dst_tznom

def aem_now(aem_tz_data_key):
    '''convienence function to return python datetime modules now() function
    params:
        aem_tz_data_key - the key into the AEM_TZ_DATA structure.(US/Eastern, etc)
    returns:
        a non-naieve datetime object with the indicated tz information
    '''
    # create a naieve datetime object
    dto = datetime.datetime.now(tz=Aem_tzinfo(AEM_TZ_DATA[aem_tz_data_key]))
    # return a datetime object that has tzinfo
    return dto.replace(tzinfo=Aem_tzinfo(AEM_TZ_DATA[aem_tz_data_key]))

def get_tz_list():
    '''return a sorted list of TZ's supported by aem_datetime'''
    tz_keys = AEM_TZ_DATA.keys()
    tz_keys.sort()
    return tz_keys

if __name__ == '__main__':
    # run a simple listing of the different timezones
    for ky in get_tz_list():
        dt = aem_now(ky)
        print ky.ljust(20), dt, datetime.datetime.tzname(dt)